﻿// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Mono.Linker.Tests.Cases.Expectations.Assertions;
using Mono.Linker.Tests.Cases.Expectations.Metadata;
using Mono.Linker.Tests.Extensions;
using Mono.Linker.Tests.TestCases;

namespace Mono.Linker.Tests.TestCasesRunner
{
    public class TestCaseCompilationMetadataProvider : BaseMetadataProvider
    {

        public TestCaseCompilationMetadataProvider(TestCase testCase, AssemblyDefinition fullTestCaseAssemblyDefinition)
            : base(testCase, fullTestCaseAssemblyDefinition)
        {

        }

        public virtual TestRunCharacteristics Characteristics =>
            TestRunCharacteristics.TargetingNetCore | TestRunCharacteristics.SupportsDefaultInterfaceMethods | TestRunCharacteristics.SupportsStaticInterfaceMethods;

        private static bool IsIgnoredByNativeAOT(CustomAttribute attr)
        {
            var ignoredBy = attr.GetPropertyValue("IgnoredBy");
            return ignoredBy is null ? true : ((Tool)ignoredBy).HasFlag(Tool.NativeAot);
        }

        public virtual bool IsIgnored([NotNullWhen(true)] out string? reason)
        {
            var ignoreAttribute = _testCaseTypeDefinition.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.Name == nameof(IgnoreTestCaseAttribute));
            if (ignoreAttribute != null && IsIgnoredByNativeAOT(ignoreAttribute))
            {
                if (ignoreAttribute.ConstructorArguments.Count == 1)
                {
                    reason = (string)ignoreAttribute.ConstructorArguments.First().Value;
                    return true;
                }
                else
                {
                    throw new ArgumentException($"Unhandled {nameof(IgnoreTestCaseAttribute)} constructor with {ignoreAttribute.ConstructorArguments} arguments");
                }
            }

            var requirementsAttribute = _testCaseTypeDefinition.CustomAttributes.FirstOrDefault(attr => attr.AttributeType.Name == nameof(TestCaseRequirementsAttribute));
            if (requirementsAttribute != null)
            {
                if (requirementsAttribute.ConstructorArguments.Count == 2)
                {
                    var testCaseRequirements = (TestRunCharacteristics)requirementsAttribute.ConstructorArguments[0].Value;

                    foreach (var value in Enum.GetValues(typeof(TestRunCharacteristics)))
                    {
                        if (IsRequirementMissing((TestRunCharacteristics)value, testCaseRequirements))
                        {
                            reason = (string)requirementsAttribute.ConstructorArguments[1].Value;
                            return true;
                        }
                    }
                }
                else
                {
                    throw new ArgumentException($"Unhandled {nameof(TestCaseRequirementsAttribute)} constructor with {requirementsAttribute.ConstructorArguments} arguments");
                }
            }

            reason = null;
            return false;
        }

        private bool IsRequirementMissing(TestRunCharacteristics requirement, TestRunCharacteristics testCaseRequirements)
        {
            return testCaseRequirements.HasFlag(requirement) && !Characteristics.HasFlag(requirement);
        }

        public virtual IEnumerable<string> GetDefines()
        {
            // There are a few tests related to native pdbs where the assertions are different between windows and non-windows
            // To enable test cases to define different expected behavior we set this special define
            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
                yield return "WIN32";

            if (Characteristics.HasFlag(TestRunCharacteristics.TargetingNetCore))
                yield return "NET";

            if (Characteristics.HasFlag(TestRunCharacteristics.SupportsDefaultInterfaceMethods))
                yield return "SUPPORTS_DEFAULT_INTERFACE_METHODS";

            foreach (var attr in _testCaseTypeDefinition.CustomAttributes.Where(attr => attr.AttributeType.Name == nameof(DefineAttribute)))
                yield return (string)attr.ConstructorArguments.First().Value;
        }

        public virtual string GetAssemblyName()
        {
            var asLibraryAttribute = _testCaseTypeDefinition.CustomAttributes
                .FirstOrDefault(attr => attr.AttributeType.Name == nameof(SetupCompileAsLibraryAttribute));
            var defaultName = asLibraryAttribute == null ? "test.exe" : "test.dll";
            return GetOptionAttributeValue(nameof(SetupCompileAssemblyNameAttribute), defaultName)!;
        }

        public virtual string GetCSharpCompilerToUse()
        {
            return GetOptionAttributeValue(nameof(SetupCSharpCompilerToUseAttribute), string.Empty)!.ToLowerInvariant();
        }

        public virtual IEnumerable<string> GetSetupCompilerArguments()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(SetupCompileArgumentAttribute))
                .Select(attr => (string)attr.ConstructorArguments.First().Value);
        }

        public virtual IEnumerable<SourceAndDestinationPair> AdditionalFilesToSandbox()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(SandboxDependencyAttribute))
                .Select(GetSourceAndRelativeDestinationValue);
        }

        private static string GetReferenceDir()
        {
            string runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
            string ncaVersion = Path.GetFileName(runtimeDir);
            string dotnetDir = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(runtimeDir)))!;
            return Path.Combine(dotnetDir, "packs", "Microsoft.NETCore.App.Ref", ncaVersion, "ref", PathUtilities.TargetFramework);
        }

        public IEnumerable<NPath> GetCommonSourceFiles()
        {
            var dam = _testCase.RootCasesDirectory.Parent
                .Combine("Mono.Linker.Tests.Cases.Expectations")
                .Combine("Support")
                .Combine("DynamicallyAccessedMembersAttribute.cs");
            yield return dam;

            var sharedDir = _testCase.RootCasesDirectory.Parent.Parent
                .Combine("src")
                .Combine("ILLink.Shared");
            yield return sharedDir.Combine("RequiresDynamicCodeAttribute.cs");
            yield return sharedDir.Combine("RequiresUnreferencedCodeAttribute.cs");
        }

        public virtual IEnumerable<string> GetCommonReferencedAssemblies(NPath workingDirectory)
        {
            yield return workingDirectory.Combine("Mono.Linker.Tests.Cases.Expectations.dll").ToString();
            if (Characteristics.HasFlag(TestRunCharacteristics.TargetingNetCore))
            {
                string referenceDir = GetReferenceDir();

                yield return Path.Combine(referenceDir, "mscorlib.dll");
                yield return Path.Combine(referenceDir, "System.Collections.dll");
                yield return Path.Combine(referenceDir, "System.Collections.Immutable.dll");
                yield return Path.Combine(referenceDir, "System.ComponentModel.TypeConverter.dll");
                yield return Path.Combine(referenceDir, "System.Console.dll");
                yield return Path.Combine(referenceDir, "System.Linq.Expressions.dll");
                yield return Path.Combine(referenceDir, "System.Memory.dll");
                yield return Path.Combine(referenceDir, "System.ObjectModel.dll");
                yield return Path.Combine(referenceDir, "System.Runtime.dll");
                yield return Path.Combine(referenceDir, "System.Runtime.Extensions.dll");
                yield return Path.Combine(referenceDir, "System.Runtime.InteropServices.dll");
            }
            else
            {
                yield return "mscorlib.dll";
            }
        }

        public virtual IEnumerable<string> GetReferencedAssemblies(NPath workingDirectory)
        {
            foreach (var fileName in GetReferenceValues())
            {

                if (fileName.StartsWith("System.", StringComparison.Ordinal) || fileName.StartsWith("Mono.", StringComparison.Ordinal) || fileName.StartsWith("Microsoft.", StringComparison.Ordinal))
                {
                    if (Characteristics.HasFlag(TestRunCharacteristics.TargetingNetCore))
                    {
                        var referenceDir = GetReferenceDir();
                        var filePath = Path.Combine(referenceDir, fileName);

                        if (File.Exists(filePath))
                        {
                            yield return filePath;
                        }
                        else
                        {
                            yield return fileName;
                        }
                    }
                    else
                    {
                        yield return fileName;
                    }
                }
                else
                {
                    // Drop any relative path information.  Sandboxing will have taken care of copying the reference to the directory
                    yield return workingDirectory.Combine(Path.GetFileName(fileName));
                }
            }
        }

        public virtual IEnumerable<string> GetReferenceDependencies()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(ReferenceDependencyAttribute))
                .Select(attr => (string)attr.ConstructorArguments[0].Value);
        }

        public virtual IEnumerable<string> GetReferenceValues()
        {
            foreach (var referenceAttr in _testCaseTypeDefinition.CustomAttributes.Where(attr => attr.AttributeType.Name == nameof(ReferenceAttribute)))
                yield return (string)referenceAttr.ConstructorArguments.First().Value;
        }

        public virtual IEnumerable<SourceAndDestinationPair> GetResources()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(SetupCompileResourceAttribute))
                .Select(GetSourceAndRelativeDestinationValue);
        }

        public virtual IEnumerable<SetupCompileInfo> GetSetupCompileAssembliesBefore()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(SetupCompileBeforeAttribute))
                .Select(CreateSetupCompileAssemblyInfo);
        }

        public virtual IEnumerable<SetupCompileInfo> GetSetupCompileAssembliesAfter()
        {
            return _testCaseTypeDefinition.CustomAttributes
                .Where(attr => attr.AttributeType.Name == nameof(SetupCompileAfterAttribute))
                .Select(CreateSetupCompileAssemblyInfo);
        }

        public bool GetGenerateTargetFrameworkAttribute()
        {
            return GetOptionAttributeValue(nameof(GetGenerateTargetFrameworkAttribute), true);
        }

        private SetupCompileInfo CreateSetupCompileAssemblyInfo(CustomAttribute attribute)
        {
            var ctorArguments = attribute.ConstructorArguments;
            return new SetupCompileInfo
            {
                OutputName = (string)ctorArguments[0].Value,
                SourceFiles = SourceFilesForAttributeArgument(ctorArguments[1]),
                References = ((CustomAttributeArgument[])ctorArguments[2].Value)?.Select(arg => arg.Value.ToString()).ToArray(),
                Defines = ((CustomAttributeArgument[])ctorArguments[3].Value)?.Select(arg => arg.Value.ToString()).ToArray(),
                Resources = ResourcesForAttributeArgument(ctorArguments[4]),
                AdditionalArguments = ((CustomAttributeArgument[])ctorArguments[5].Value)?.Select(arg => arg.Value.ToString()).ToArray(),
                CompilerToUse = (string)ctorArguments[6].Value,
                AddAsReference = ctorArguments.Count >= 8 ? (bool)ctorArguments[7].Value : true,
                RemoveFromLinkerInput = ctorArguments.Count >= 9 ? (bool)ctorArguments[8].Value : false,
                OutputSubFolder = ctorArguments.Count >= 10 ? (string)ctorArguments[9].Value : null
            };
        }

        protected NPath[] SourceFilesForAttributeArgument(CustomAttributeArgument attributeArgument)
        {
            return ((CustomAttributeArgument[])attributeArgument.Value)
                .Select(attributeArg => SourceFileForAttributeArgumentValue(attributeArg.Value))
                .Distinct()
                .ToArray();
        }

        protected SourceAndDestinationPair[]? ResourcesForAttributeArgument(CustomAttributeArgument attributeArgument)
        {
            return ((CustomAttributeArgument[])attributeArgument.Value)
                ?.Select(arg =>
                {
                    var referenceArg = (CustomAttributeArgument)arg.Value;
                    if (referenceArg.Value is string source)
                    {
                        var fullSource = MakeSourceTreeFilePathAbsolute(source);
                        return new SourceAndDestinationPair
                        {
                            Source = fullSource,
                            DestinationFileName = fullSource.FileName
                        };
                    }
                    var sourceAndDestination = (CustomAttributeArgument[])referenceArg.Value;
                    return new SourceAndDestinationPair
                    {
                        Source = MakeSourceTreeFilePathAbsolute(sourceAndDestination[0].Value!.ToString()!),
                        DestinationFileName = sourceAndDestination[1].Value!.ToString()!
                    };
                })
                ?.ToArray();
        }
    }
}
